package link;

import java.io.IOException;
import java.util.Observable;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.ReentrantLock;

import link.correction.Hamming8_4Adder;
import link.correction.Hamming8_4Corrector;
import link.correction.NotCorrectableException;
import lpt.*;

public class LptConnection extends Observable implements Runnable, LinkInterface{
	
	protected Lpt lpt;	
	protected LinkState ls;	
	protected ReentrantLock sendQueueLock = new ReentrantLock();
	protected Queue<byte[]> sendQueue; //queue holds FRAMES (not packets)
	protected long lastSendTime;
	protected ReentrantLock recQueueLock = new ReentrantLock();
	protected Queue<byte[]> recQueue; //queue holds FRAMES (not packets)
	
	private long sendrecStartTime;
	
	
	protected boolean running;
	
	public LptConnection(){
		lpt = new ErrorLptAdapter();
		lpt.writeLPT(0x00);
		ls = LinkState.WAITING;
		sendQueue = new ConcurrentLinkedQueue<byte[]>();
		recQueue = new ConcurrentLinkedQueue<byte[]>();
	}
	
	/**
	 * frames the packet and adds it to the queue
	 * @param pk the packet to queue
	 * @requires 10 <= pk.length <= 258
	 */
	public void send(byte[] pk){
		if(pk.length > Constants.MAX_PAYLOAD_SIZE)
			throw new IllegalArgumentException("Packet length > MAX_PAYLOAD_SIZE ("+pk.length);
		sendQueueLock.lock();
		sendQueue.add(Framer.frame(pk));
		sendQueueLock.unlock();
	}
	
	/**
	 * returns the first packet in the Queue, this packet will after that no longer
	 * be in the queue. Returns null when there is no item in the queue
	 * @return first packet in the queue or null when the queue is empty
	 */
	public byte[] getPacket() {
		byte[] ret;
		recQueueLock.lock();
		ret = recQueue.poll();
		recQueueLock.unlock();
		return ret;
	}
	
	public LinkState getState(){
		return ls;
	}

	@Override
	public void run() {
		running = true;
		lpt.writeLPT(0x00);
		while(running){
			if(ls == LinkState.WAITING){
				if(ones(lpt.readLPT()) > 3){ //if the other side is trying to send
					ls = LinkState.RECEIVING;
					byte[] buf = new byte[Constants.MAX_FRAME_SIZE];
					int length = rec(buf);
					lpt.writeLPT(0x00);
					ls = LinkState.WAITING;
					if(length == -1){ // this packet could not be corrected, ignore it
						System.err.println("Uncorrectable packet");
						continue;
					} else if(length == -2){ // this packet could not be corrected, ignore it
						System.err.println("Timeout while receiving");
						lpt.writeLPT(0x00);
						waitAfterTimeOut();
						continue;
					}
					byte[] r = new byte[length];
					System.arraycopy(buf, 0, r, 0, length);
					recQueueLock.lock();
					recQueue.add(r);
					recQueueLock.unlock();
					setChanged();
					notifyObservers();
				} else if(!sendQueue.isEmpty() && (System.currentTimeMillis() - lastSendTime) > (Constants.SEND_WAIT_TIME + Math.random() * Constants.SEND_WAIT_TIME_RAND)){
					boolean hadTimeOut = false;
					ls = LinkState.SENDING;
					recQueueLock.lock();
					byte [] f = sendQueue.poll();
					recQueueLock.unlock();
					try{
						sendFrame(f);
					} catch (TimeOutException e) {
						System.err.println("Timeout while sending");
						hadTimeOut = true;
					}
					lpt.writeLPT(0x00); //set all zeroes on the cable to indicate we are waiting
					lastSendTime = System.currentTimeMillis();
					ls = LinkState.WAITING;
					if(hadTimeOut)
						waitAfterTimeOut();
				}
			}
		}		
	}
	
	public void stop(){
		running = false;
		lpt.writeLPT(0x00);
	}
	
	//runs in the run()-thread
	/**
	 * receives a frame from the LPT-cable
	 * @param buf the buffer the payload from the frame to
	 * @return the length of the payload, returns -1 if the frame could not be decoded
	 */
	protected int rec(byte[] buf) {
		startSendRecTimeout();
		//first handle the startSequence
		while(lpt.readLPT()  != 0x0F){ //right now the startsequence with strobe 1 is on the cable
			if(checkSendRecTimeout()) return -2;
			lpt.writeLPT(0xFF); //ack ss with strobe 1
		}
		lpt.writeLPT(0x00); //ack the ss with strobe 0
		//now handle the data
		int i = 0; //position to write to in the buffer
		try{
			while(true){
				if(i > Constants.MAX_PAYLOAD_SIZE) {					
					System.err.println("PAKKET TE LANG! - ");
					readRealByte(); //frame is to long, ignore bytes larger then frame
				} else {
					buf[i] = (byte)readRealByte();
					i++;
				}
			}
		} catch (NotCorrectableException e){
			i = 0; //return -1
		} catch (TimeOutException e){
			i = -1; //return -2
		} catch(IOException e){
			//this means the tranmission is over
		} 
		return i-1;
	}
	
	//runs in the run()-thread
	/**
	 * Reads a real byte from the LPT, thus it reads two bytes (4 nibbles) from the 
	 * LPT, those two bytes are then corrected by Hamming8_4Corrector
	 * @return the read byte, corrected by hamming8_4Corrector
	 * @throws IOException 
	 */
	protected int readRealByte() throws IOException{
		byte[] cor = new byte[2];
		//we need to read two bytes (4 nibbles) from the LPT
		byte b1a = readNibble(); //rightmost 4 bits
		byte b1b = readNibble(); //leftmost 4 bits
		cor[0] = (byte) ((b1a & 0x0F) | ((b1b << 4) & 0xF0));
		byte b2a = readNibble();
		byte b2b = readNibble();
		cor[1] = (byte) ((b2a & 0x0F) | ((b2b << 4) & 0xF0));
		byte[] res = Hamming8_4Corrector.correct(cor);
		return res[0];
	}
	
	//runs in the run()-thread
	/**
	 * Reads a nibble from the LPT, thus it reads once, acks, reads
	 * the same data again and returns
	 * @return the nibble read, the nibble is in the rightmost 4 bits
	 * 			of the returned byte
	 */
	protected byte readNibble() throws IOException{
		byte data = 0x00;
		boolean couldBeStop = false;
		while((lpt.readLPT() & 0x10) != 0x10){ //while strobe == 0
			if(checkSendRecTimeout()) throw new TimeOutException();
			lpt.writeLPT(0x00); //indicate we are ready for the next nibble
			//it could be that this while()-loop is never executed when running this method
		}
		while((lpt.readLPT() & 0x10) == 0x10){ //while strobe == 1	
			if(checkSendRecTimeout()) throw new TimeOutException();
			data = (byte) (lpt.readLPT() & 0x0F);
			lpt.writeLPT(0xFF); //indicate we received the strobe
			couldBeStop = data == 0x0F;
		}
		//strobe is now 0
		data = (byte) (lpt.readLPT() & 0x0F);
		if(couldBeStop){ //make sure we did not just misread the strobe
						// the sender will keep resending while we wait
			try {
				Thread.sleep(Constants.COULD_BE_STOP_SLEEP_TIME);
			} catch (InterruptedException e) {}
			data = (byte) (lpt.readLPT() & 0x0F);
		}
		lpt.writeLPT(0x00); //indicate we have read the data
		if(data == 0x00 && couldBeStop) throw new IOException("End of tranmission");
		return data;
	}

	//runs in the run()-thread
	/**
	 * send the frame fr over the LPT-cable.
	 * the frame is expected to be complete
	 * thus to also have the start-sequence
	 * @param fr the packet to send
	 * @throws TimeOutException 
	 */
	protected void sendFrame(byte[] fr) throws TimeOutException{
		startSendRecTimeout();
		//write the startSequence with probe 1
		//and wait for the ack, this should be all ones as the waiting state
		//requires the output to be all ones
		sendAndWait((byte)0xFF, true);
		//write the startSequence with strobe 0, is acked by all zeroes
		sendAndWait((byte)0x0F, false);
		//now send all other nibbles
		byte[] nib = toNibbles(fr);
		for(int i=0; i<nib.length; i++){
			sendAndWait((byte)(nib[i] | 0x10), true); //strobe 1
			sendAndWait((byte)(nib[i] & 0x0F), false); //strobe 0
		}
		//now send the stop sequence
		sendAndWait((byte)0x1F, true);
		sendAndWait((byte)0x00, false);
	}
	
	//runs in the run()-thread
	/**
	 * This method will block until the expected Ack is send over the cable
	 * when the parameter is true an Ack will be considered received when
	 * three or more ones are on the cable, when the parameter is false
	 * an Ack is considered to be received when there are three or more zeroes
	 * on the cable.
	 * @param toSend the data to send
	 * @param allOnes whether the ACK is expected to be all ones (true) or all zeroes (false)
	 * @throws TimeOutException 
	 */
	protected void sendAndWait(byte toSend, boolean allOnes) throws TimeOutException{
		int i;
		while(true){
			if(checkSendRecTimeout()) throw new TimeOutException();
			lpt.writeLPT(toSend);
			i = ones(lpt.readLPT());
			if(allOnes && i > 3)
				return;
			else if(!allOnes && i < 3)
				return;
		}
	}
	
	/**
	 * Counts number of 1s in the given byte. Uses a rather
	 * ineffecient/naive method, which should be fast enough for 
	 * 5 bits
	 * @param n the byte to count the number of ones 1
	 * @return the number of 1s in n
	 * @ensures 0<= \result <= 5
	 */
	protected static int ones(byte n){
		int res = 0;
		for(int i=0; i<5; i++)
			if((n & (0x01<<i)) == (0x01<<i)) res++;
		return res;
	}
	
	
	/**
	 * returns the nibbles in the provided byte[], every index i from 
	 * the bs-array is presented in \result[i*2] for the rightmost bits and
	 * \result[1*2+1] for the leftmost bits. The significant bits of the
	 * nibble are in the rightmost bits of \result[i] array
	 * @param bs the bytes to split into nibbles
	 * @return an array containing the two nibbles
	 * @ensures \result.length == bs.length * 2
	 */
	protected static byte[] toNibbles(byte[] bs){
		byte[] res = new byte[bs.length*2];
		for(int i=0; i<bs.length; i++){
			res[i*2] |= (bs[i] & 0x0F);
			res[i*2+1] |= (bs[i] >> 4) & 0x0F;
		}
		return res;
	}
	
	protected static class Framer{
		
		private static Hamming8_4Adder h84a = new Hamming8_4Adder();
		
		/**
		 * frames the packet pk, adding in the length in bytes
		 * the maximum length of a packet is 258 bytes, due to the minimum length of 10 
		 * bytes because of the header lengths of the network and transport layer
		 * this means that the length is expressed in one byte, the actual length of the packet is
		 * (length + 10) * 2 //the length of the payload of this frame, multiplied by 2 because of (8,4)-Hamming
		 * + 2 //the length of the lentgh-field (note that this also has (8,4)-hamming code)
		 * the layout of the frame is:
		 * length 		| data
		 * 2 bytes (8,4)  <n> bytes (8,4)
		 * @param pk the packet to frame
		 * @return a frame containing this packet, startsequence and length
		 * @require 10 <= pk.length <= 258
		 * @ensures \result.length = (pk.length + 1)*2
		 */
		public static byte[] frame(byte[] pk){
			byte[] res = new byte[(pk.length + 1)*2];
			//add length
			/*byte[] t = new byte[1];
			t[0] = (byte)(pk.length-10);
			System.arraycopy(h84a.addCorrection(t), 0, res, 1, 2);*/
			//add data
			System.arraycopy(h84a.addCorrection(pk), 0, res, 0, pk.length*2);
			return res;
		}
		
	}
	
	/**
	 * checks whether the send/rec timeout has passed
	 * @return true when the timeout has passed, false when there still is time
	 */
	protected boolean checkSendRecTimeout(){
		return System.currentTimeMillis() > (sendrecStartTime + Constants.SEND_REC_TIMEOUT);
	}
	
	/**
	 * starts the sendRecTimeout
	 */
	protected void startSendRecTimeout(){
		this.sendrecStartTime = System.currentTimeMillis();
	}
	
	/**
	 * wait for the required amount of time after a timeout
	 */
	protected void waitAfterTimeOut(){
		try {
			Thread.sleep(Constants.AFTER_TIMEOUT_WAIT);
		} catch (InterruptedException e) { }
		this.lastSendTime = System.currentTimeMillis(); //so we wait a random amount of time before sending again		
	}
	
	@Override
	public String toString(){
		return "LPT Connection";
	}

	
}
